package me.smartproxy.tunnel;

import android.annotation.SuppressLint;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;

import me.smartproxy.core.LocalVpnService;
import me.smartproxy.core.ProxyConfig;

public abstract class Tunnel {

	final static ByteBuffer GL_BUFFER=ByteBuffer.allocate(20000);
	public static long SessionCount;
 
    protected abstract void onConnected(ByteBuffer buffer) throws Exception;
    protected abstract boolean isTunnelEstablished();
    protected abstract void beforeSend(ByteBuffer buffer) throws Exception;
    protected abstract void afterReceived(ByteBuffer buffer) throws Exception;
    protected abstract void onDispose();
    
	private SocketChannel m_InnerChannel;
	private ByteBuffer m_SendRemainBuffer;
	private Selector m_Selector;
	private Tunnel m_BrotherTunnel;
	private boolean m_Disposed;
    private InetSocketAddress m_ServerEP;
    protected InetSocketAddress m_DestAddress;

	public Tunnel(SocketChannel innerChannel,Selector selector){
		this.m_InnerChannel=innerChannel;
		this.m_Selector=selector;
		SessionCount++;
	}
	
	public Tunnel(InetSocketAddress serverAddress,Selector selector) throws IOException{
		SocketChannel innerChannel=SocketChannel.open();
		innerChannel.configureBlocking(false);
		this.m_InnerChannel=innerChannel;
		this.m_Selector=selector;
		this.m_ServerEP=serverAddress;
		SessionCount++;
	}

	public void setBrotherTunnel(Tunnel brotherTunnel){
		m_BrotherTunnel=brotherTunnel;
	}
	
	public void connect(InetSocketAddress destAddress) throws Exception{
		if(LocalVpnService.Instance.protect(m_InnerChannel.socket())){//����socket����vpn
			m_DestAddress=destAddress;
			m_InnerChannel.register(m_Selector, SelectionKey.OP_CONNECT,this);//ע�����¼�
			m_InnerChannel.connect(m_ServerEP);//����Ŀ��
		}else {
			throw new Exception("VPN protect socket failed.");
		}
	}
  
	protected void beginReceive() throws Exception{
		if(m_InnerChannel.isBlocking()){
			m_InnerChannel.configureBlocking(false);
		}
		m_InnerChannel.register(m_Selector, SelectionKey.OP_READ,this);//ע���¼�
	}
	

	protected boolean write(ByteBuffer buffer,boolean copyRemainData) throws Exception {
		int bytesSent;
    	while (buffer.hasRemaining()) {
			bytesSent=m_InnerChannel.write(buffer);
			if(bytesSent==0){
				break;//�����ٷ���ˣ���ֹѭ��
			}
		}
    	
    	if(buffer.hasRemaining()){//���û�з������
    		if(copyRemainData){//����ʣ����ݣ�Ȼ�����д���¼�������д��ʱд�롣
    			//����ʣ�����
    			if(m_SendRemainBuffer==null){
    				m_SendRemainBuffer=ByteBuffer.allocate(buffer.capacity());
    			}
    			m_SendRemainBuffer.clear();
        		m_SendRemainBuffer.put(buffer);
    			m_SendRemainBuffer.flip();
    			m_InnerChannel.register(m_Selector,SelectionKey.OP_WRITE, this);//ע��д�¼�
    		}
			return false;
    	}
    	else {//��������
    		return true;
		}
	}
 
    protected void onTunnelEstablished() throws Exception{
		this.beginReceive();//��ʼ�������
		m_BrotherTunnel.beginReceive();//�ֵ�Ҳ��ʼ����ݰ�
    }

    @SuppressLint("DefaultLocale")
	public void onConnectable(){
    	try {
        	if(m_InnerChannel.finishConnect()){//���ӳɹ�
        		onConnected(GL_BUFFER);//֪ͨ���TCP����ӣ������Ը���Э��ʵ�����ֵȡ�
        	}else {//����ʧ��
        		LocalVpnService.Instance.writeLog("Error: connect to %s failed.",m_ServerEP);
				this.dispose();
			}
		} catch (Exception e) {
			LocalVpnService.Instance.writeLog("Error: connect to %s failed: %s", m_ServerEP,e);
			this.dispose();
		}
    }
    
	public void onReadable(SelectionKey key){
		try {
			ByteBuffer buffer=GL_BUFFER;
			buffer.clear();
			int bytesRead=m_InnerChannel.read(buffer);
			if(bytesRead>0){
				buffer.flip();
				afterReceived(buffer);//������ദ�����������ݡ�
				if(isTunnelEstablished()&&buffer.hasRemaining()){//����������ݣ�ת�����ֵܡ�
					m_BrotherTunnel.beforeSend(buffer);//����֮ǰ��������ദ����������ܵȡ�
					if(!m_BrotherTunnel.write(buffer,true)){
						key.cancel();//�ֵܳԲ������ȡ���ȡ�¼���
						if(ProxyConfig.IS_DEBUG)
							System.out.printf("%s can not read more.\n", m_ServerEP);
					}
				} 
			}else if(bytesRead<0) {
				this.dispose();//�����ѹرգ��ͷ���Դ��
			}
		} catch (Exception e) {
			e.printStackTrace();
			this.dispose();
		}
	}

	public void onWritable(SelectionKey key){
		try {
			this.beforeSend(m_SendRemainBuffer);//����֮ǰ��������ദ����������ܵȡ�
			if(this.write(m_SendRemainBuffer, false)) {//��ʣ������Ѿ��������
				key.cancel();//ȡ�д�¼���
				if(isTunnelEstablished()){
					m_BrotherTunnel.beginReceive();//�����ݷ�����ϣ�֪ͨ�ֵܿ��������ˡ�
				}else {
					this.beginReceive();//��ʼ���մ��������Ӧ���
				}
			}
		} catch (Exception e) {
			this.dispose();
		}
	}
	
	public void dispose(){
		disposeInternal(true);
	}
	
	void disposeInternal(boolean disposeBrother) {
		if(m_Disposed){
			return;
		}
		else {
			try {
				m_InnerChannel.close();
			} catch (Exception e) {
			}
			
			if(m_BrotherTunnel!=null&&disposeBrother){
				m_BrotherTunnel.disposeInternal(false);//���ֵܵ���ԴҲ�ͷ�ˡ�
			}

			m_InnerChannel=null;
		    m_SendRemainBuffer=null;
			m_Selector=null;
			m_BrotherTunnel=null;
			m_Disposed=true;
			SessionCount--;
			
			onDispose();
		}
	}
}